Leaflet Blog in Deno Fresh
1/** @jsxImportSource preact */
2import { CSS, render } from "@deno/gfm";
3import { Handlers, PageProps } from "$fresh/server.ts";
4
5import { Layout } from "../../islands/layout.tsx";
6import { PostInfo } from "../../components/post-info.tsx";
7import { Title } from "../../components/typography.tsx";
8import { getPost } from "../../lib/api.ts";
9import { Head } from "$fresh/runtime.ts";
10
11interface Post {
12 uri: string;
13 value: {
14 title: string;
15 content: string;
16 createdAt: string;
17 };
18}
19
20// Only override backgrounds in dark mode to make them transparent
21const transparentDarkModeCSS = `
22@media (prefers-color-scheme: dark) {
23 .markdown-body {
24 color: white;
25 background-color: transparent;
26 }
27
28 .markdown-body a {
29 color: #58a6ff;
30 }
31
32 .markdown-body blockquote {
33 border-left-color: #30363d;
34 background-color: transparent;
35 }
36
37 .markdown-body pre,
38 .markdown-body code {
39 background-color: transparent;
40 color: #c9d1d9;
41 }
42
43 .markdown-body table td,
44 .markdown-body table th {
45 border-color: #30363d;
46 background-color: transparent;
47 }
48}
49
50.font-sans { font-family: var(--font-sans); }
51.font-serif { font-family: var(--font-serif); }
52.font-mono { font-family: var(--font-mono); }
53
54.markdown-body h1 {
55 font-family: var(--font-serif);
56 text-transform: uppercase;
57 font-size: 2.25rem;
58}
59
60.markdown-body h2 {
61 font-family: var(--font-serif);
62 text-transform: uppercase;
63 font-size: 1.75rem;
64}
65
66.markdown-body h3 {
67 font-family: var(--font-serif);
68 text-transform: uppercase;
69 font-size: 1.5rem;
70}
71
72.markdown-body h4 {
73 font-family: var(--font-serif);
74 text-transform: uppercase;
75 font-size: 1.25rem;
76}
77
78.markdown-body h5 {
79 font-family: var(--font-serif);
80 text-transform: uppercase;
81 font-size: 1rem;
82}
83
84.markdown-body h6 {
85 font-family: var(--font-serif);
86 text-transform: uppercase;
87 font-size: 0.875rem;
88}
89`;
90
91export const handler: Handlers<Post> = {
92 async GET(_req, ctx) {
93 try {
94 const { slug } = ctx.params;
95 const post = await getPost(slug);
96 return ctx.render(post);
97 } catch (error) {
98 console.error("Error fetching post:", error);
99 return new Response("Post not found", { status: 404 });
100 }
101 },
102};
103
104export default function BlogPage({ data: post }: PageProps<Post>) {
105 if (!post) {
106 return <div>Post not found</div>;
107 }
108
109 return (
110 <>
111 <Head>
112 <title>{post.value.title} — knotbin</title>
113 <meta name="description" content="by Roscoe Rubin-Rottenberg" />
114 {/* Merge GFM's default styles with our dark-mode overrides */}
115 <style
116 dangerouslySetInnerHTML={{ __html: CSS + transparentDarkModeCSS }}
117 />
118 </Head>
119
120 <Layout>
121 <div class="p-8 pb-20 gap-16 sm:p-20">
122 <link rel="alternate" href={post.uri} />
123 <div class="max-w-[600px] mx-auto">
124 <article class="w-full space-y-8">
125 <div class="space-y-4 w-full">
126 <Title>{post.value.title}</Title>
127 <PostInfo
128 content={post.value.content}
129 createdAt={post.value.createdAt}
130 includeAuthor
131 className="text-sm"
132 />
133 <div class="diagonal-pattern w-full h-3" />
134 </div>
135 <div class="[&>.bluesky-embed]:mt-8 [&>.bluesky-embed]:mb-0">
136 <div
137 class="mt-8 markdown-body"
138 // replace old pds url with new one for blob urls
139 dangerouslySetInnerHTML={{
140 __html: render(post.value.content).replace(
141 /puffball\.us-east\.host\.bsky\.network/g,
142 "knotbin.xyz",
143 ),
144 }}
145 />
146 </div>
147 </article>
148 </div>
149 </div>
150 </Layout>
151 </>
152 );
153}